[Previous] [Next]

WebClasses

Visual Basic 6 adds a new tool to your bag of Internet programming techniques: the WebClass component. A WebClass is an in-process component that runs inside IIS and that intercepts and then processes all the requests that clients make to an ASP document. Figure 20-13 diagrams how a WebClass works.

Click to view at full size.

Figure 20-13. A WebClass acts as an intermediary between the browser and ASP.

Before diving into the details, let me clarify one point: WebClasses can't do anything that you can't do already with an ASP script and a custom component written in Visual Basic (or another language capable of delivering ActiveX DLLs). The difference is in how the technologies work at a lower level and in how the programmer uses them. In a standard ASP application, the ASP script takes the control when the client browser requests an ASP document and relinquishes it when the resulting HTML page is sent back to the client. In a WebClass application, the WebClass comes into existence when the client browser references a page in the application, and from that point on it reacts to the actions that the user performs on the page, such as clicking a hyperlink or a Submit button.

A key advantage of WebClasses over traditional script-based and component-based ASP programming is that you develop a WebClass application entirely within the Visual Basic environment, so you have all the usual debugging tools at hand. Because a WebClass usually comprises multiple pages, it should be considered a higher level component. For example, a single WebClass can implement an entire online order management system and can therefore be reused in another application much easier than a collection of loosely coupled ASP pages can. Other evidence of WebClasses' higher level of abstraction is that you don't need to do anything special to maintain the state of a session between consecutive requests from users—such as use cookies or Session variables—because the WebClass does everything for you, even if at the expense of some scalability. (You can decide to disable this option and produce more scalable but stateless WebClass components.) Finally, unlike ASP scripts, the code for the WebClass is isolated from the HTML code that affects the appearance of its pages; therefore, it's easier to partition the task of building the application among developers and HTML authors.

First Impressions

You create a WebClass by using a special designer provided with Visual Basic 6. Unlike the DHTMLPage designer, which lets you create an entire page from scratch without resorting to an external HTML editor, the WebClass designer is meant to import HTML pages created outside the Visual Basic environment. What the designer does is graphically display all the items in an HTML page that are capable of sending requests to the server, such as the ACTION attribute of a FORM tag or the HREF attribute of a hyperlink element. In general, all the tags and attributes that can contain a URL are candidates for being the source of a request to the server. You can then associate such items with actions that the WebClass will perform when that particular request is received. In a sense, this scenario is nothing but the Visual Basic event-driven programming model applied to IIS and ASP applications.

Creating an IIS project

You create a WebClass by selecting the IIS Application project type from the project gallery. This template project includes one WebClass module and has all of the necessary project attributes already set for you. An IIS project is an ActiveX DLL project whose threading model is set to Apartment Threading and that contains one or more WebClass designer modules. In the General tab of the Project Properties dialog box, you'll also find that this project is marked for unattended execution (which makes sense, because it will run under IIS) and that the Retained In Memory flag is set. When this flag is set, the Visual Basic run-time library won't be unloaded even if no WebClass components are currently running in IIS. This arrangement allows the components to be instantiated very quickly when a request comes from a client.

WebClass modules have some properties of their own. They have a Name property (the name used inside the current project to reference the WebClass from Visual Basic code), a NameInURL property (the name used when referencing this class from HTML and ASP code), a Public property (can only be True), and a StateManagement property.

The StateManagement property indicates what happens between consecutive requests from a client. If this property is set to 1-wcNoState (the default value), the WebClass component is automatically destroyed after it sends a response to the client browser; if this property is set to 2-wcRetainInstance, the WebClass component is kept alive between consecutive requests from the same client. Each option has its pros and cons. If the instance of the WebClass is retained, all the variables in the WebClass module are automatically preserved between consecutive requests, which makes the programmer's job much easier. On the other hand, each component running on the server takes memory and CPU resources, so setting 1-wcNoState creates more scalable solutions—but at the cost of some added complexity in programming. (See the section "State Management" later in this chapter for more details.)

A WebClass contains and manages one or more WebItems. Each WebItem corresponds to an HTML page that is sent back to the client browser. There are two types of WebItems: HTML template WebItems and custom WebItems. An HTML template WebItem is based on an existing HTML page that's used as a template for building the response page. This page is then sent to the client browser, usually after substituting one or more placeholders with data. A custom WebItem doesn't correspond to any existing HTML page, and it builds the page returned to the client browser using only code, typically with a series of Response.Write commands. A WebClass can contain only HTML template WebItems, only custom WebItems, or (more often) a mixture of the two types.

The first thing to do when working on an IIS application is to establish the directory structure of the project. To separate all the items in the project in an orderly manner, you need at least three directories:

You can use a single directory for the three different types of files used by the project if you want to, though it isn't usually a good idea. If you create an HTML template file in the same directory where the WebClass source file is located, the designer will automatically create a new HTML file whose name is obtained by appending a number to the original name. For example, if you have an Order.htm template file, the designer will create an Order1.htm file in the same directory. Such a rename operation doesn't occur if the original template file is located in a directory different from the one in which the WebClass project is stored. If you work with numerous template files, you'll prefer having those generated by the WebClass differentiated.

Adding HTML Template WebItems

HTML Template WebItems are undoubtedly the simplest WebItems to work with. To create such a WebItem, you must have prepared an HTML template file using an HTML editor such as Microsoft FrontPage or Microsoft InterDev. When creating such a template file, you don't need to pay attention to the destination of hyperlinks and other URLs in the page because they'll be replaced by the WebClass designer anyway. The same applies to the ACTION attribute of forms, to the SRC attribute of the IMG tag, and other HTML attributes that can be assigned a URL. You can't directly associate an event with a button on a form, but you need to associate an event with the ACTION attribute of the form that contains that button. The type of the button must be SUBMIT.

Before importing any HTML template file, you must first save the IIS application to disk. This step is necessary because Visual Basic must know where the modified HTML template file is to be stored. As mentioned in the previous section, it's usually a good idea to keep the original HTML files in a separate directory so that you don't force Visual Basic to create a different name for the modified template file. After you've saved the project, you can import an HTML template file by clicking the fifth button from the left in the WebClass designer's toolbar. This operation creates a new HTML Template WebItem, which you can rename with a meaningful name: This is the name that will be used in code to refer to that WebItem.

Figure 20-14 shows the WebClass designer after importing two HTML template files. As you can see, the StartPage WebItem contains three items that can send requests to the server and that can raise an event in the WebClass: the BACKGROUND attribute of the BODY element and two hyperlinks. Even if the designer displays all the possible sources for requests, in most cases, you can focus on just a small subset of them—such as the hyperlinks, ACTION attributes in FORM tags, and SRC attributes in IMG tags. If the tag in the original HTML file is associated with an ID, this ID will be used to identify the tag in the designer; otherwise, the designer automatically generates unique IDs for each tag capable of raising an event. For example, the first hyperlink in the page that doesn't have an ID is assigned the Hyperlink1 ID, the second hyperlink without an ID is assigned the Hyperlink2 ID, and so on. Such IDs are temporary and aren't stored in the HTML file unless you connect the tag to an event. If you connect these tags to a WebItem, the ID becomes permanent and is stored in the HTML file.

NOTE
If the HTML template file contains errors—for example, unmatched opening and closing tags—you get an error when importing the template into the designer. In addition, you can only import forms whose METHOD attributes are set to POST because only forms of this type send the server a request that can be trapped by the WebClass. If you add an HTML template WebItem containing a form that uses the GET method, the WebClass designer displays a warning and then automatically modifies the form's METHOD attribute to the POST value.

Click to view at full size.

Figure 20-14. The WebClass designer, after creating two HTML Template WebItems.

Connecting a WebItem

To activate one of these potential sources for requests (requests that become events in the WebClass), you must connect it to a WebItem or a custom event, which you do by right-clicking on such a source in the right pane of the WebClass designer. For now, let's focus on the Connect To WebItem menu command, which brings up the dialog box shown in Figure 20-15. In this dialog box, you can select any of the WebItems currently defined in the WebClass. (You can't connect an attribute to a WebItem defined in another WebClass.) After you close the dialog box, the selected WebItem's name appears in the Target column in the right pane of the designer, near the attribute it has been connected to.

Figure 20-15. This dialog box appears when you connect an attribute of a tag in an HTML template to a WebItem defined in the WebClass.

You can't edit the original HTML template from inside the Visual Basic project, but you can quickly invoke your favorite HTML editor on that page, using a button on the designer's toolbar. You can configure which editor should be launched by setting the External HTML Editor option in the Advanced tab of the Options dialog box of the Tools menu. The Visual Basic environment continuously monitors the date and time of HTML template files, and as soon as it finds that one of them has changed, it asks you if you want to reload and parse the new version of the file into the designer. You can also refresh a template in another way, by right-clicking on an HTML Template WebItem and choosing the Refresh HTML Template command from the pop-up menu. This command is useful if for some reason the Visual Basic environment doesn't correctly recognize that the HTML template file has been changed. The designer is usually able to maintain all the associations you set previously, so you don't have to reconnect attributes to WebItems each time you edit the HTML template file.

If the HTML file references other HTML files, these files should be manually copied in the same directory as the WebClass project or in one of its subdirectories. For the same reason, all URLs need to be relative to the current directory so that you can freely copy these files to the deployment directory without having to edit them. Using absolute URLs is acceptable in only two cases: when you refer to files that are always in the same position in your Web site and when you refer to files located at another Web site.

Writing code

You must write some code inside the WebClass designer before running the project. This code is necessary because the WebClass doesn't know what to do when it is activated, and it doesn't know which WebItem should be sent to the client when the WebClass application is first activated.

You decide what happens when the WebClass application is activated—that is, when the browser references its main ASP file—by writing code in the WebClass's Start event. In this event, you typically redirect the browser to a WebItem by assigning a WebItem reference to the WebClass's NextItem property, as in the following piece of code:

' This event fires when the WebClass is activated for the first time,
' that is, when a client browser references its main ASP file.
Private Sub WebClass_Start()
    Set NextItem = StartPage
End Sub

Starting the processing of a WebItem doesn't automatically send it to the client's browser. In fact, when a WebItem is assigned to the NextItem property, what happens is that the WebClass fires it's the WebItem's Respond event. In this event procedure, you might want to do additional processing—for example, you might query a database and retrieve the values that must be displayed in the client's browser. The assignment to the NextItem property doesn't alter the execution flow immediately because Visual Basic fires the target WebItem's Respond event only when the current event procedure has completed.

When you're ready to send the data to the browser, you can invoke the WebItem's WriteTemplate method as in the following piece of code:

' This event fires when the user jumps to the StartPage page.
Private Sub StartPage_Respond()
    StartPage.WriteTemplate
End Sub

At this point, the client's browser displays the StartPage.htm page. In this particular case, the start page doesn't contain any portions that are to be substituted, so the WebClass sends it unmodified to the client. (This case isn't the norm, however.) When the user clicks on a hyperlink, the WebClass receives the Respond event of the WebItem that is connected to that hyperlink. Again, you reply to this event by executing the WriteTemplate method of the involved WebItem:

' This event fires when the user clicks the hyperlink on the 
' StartPage page that is linked to the QueryOrder WebItem.
Private Sub QueryOrder_Respond()
    QueryOrder.WriteTemplate
End Sub

At first you might be puzzled by the amount of code you have to write just to run a simple application like this one. Don't forget, however, that the WebClass designer really shines when the pages sent back to the client contain dynamic data.

You can run the application built so far: the Visual Basic environment will start IIS and will load the start page into Internet Explorer. Set a breakpoint in the WebClass_Start event to see what happens when you click on a hyperlink. Remember that you can debug a WebClass only on the machine where IIS is running. Also, it is generally preferable to have only one instance of the browser running during the debug phase because Visual Basic doesn't keep track of which instance is showing the output coming from the WebClass, and all the instances might be affected during the debug session.

Extending the example

The best way to learn how to use WebClasses is to see a complete example in action. For this reason, I've prepared a nontrivial IIS application based on the NorthWind database that comes with SQL Server 7. This sample application lets users do three different things:

Figure 20-16 sketches the outline of the sample application. As you can see, there are eight template WebItems and two custom WebItems. The figure doesn't show all the possible hyperlinks, such as the hyperlinks that bring users back to the StartPage WebItem at the end of a search or after the confirmation or cancellation of an order.

NOTE
To have the sample application work correctly, you must create a system DSN named "NorthWind" that points to the NorthWind SQL Server 7 database. If you don't have SQL Server 7 installed, you can create a version of this database that works under SQL Server 6.5 database using Access's Upsizing Wizard to convert the NWind.mdb database.

Click to view at full size.

Figure 20-16. The structure of the sample application.

The most complex WebItem in the application is Products, for many reasons. This WebItem has to display the input fields for entering search criteria and then display a table with the results of the current search. In general, if the page contains an HTML table with a variable number of rows, you need a custom WebItem because you have to generate the table dynamically using the Response.Write method, exactly as you would in a regular ASP application that doesn't rely on WebClasses.

NOTE
As I'm writing this, Microsoft is launching a new Web site at http://vblive.rte.microsoft.com. This site is completely written using WebClasses, and it's the best place to see the potential this technology offers. Even more astonishing is that you can download the complete Visual Basic source code of the site, so you can learn dozens of tricks to make the best use of WebClasses.

WebClass Basic Techniques

Now that you have a basic idea of what a WebClass is, let's see how you can use it to solve the most common problems you'll encounter when developing IIS applications.

Accessing the ASP object model

One of the advantages of the WebClass programming model is that all the objects in the ASP object model are accessible as properties of the WebClass itself. For example, to write HTML code to the output stream, you can use the Response object, as follows:

Response.Write "<BODY>" & vbCrLf

Besides the main ASP objects—that is, Request, Response, Server, Application, and Session—the WebClass also makes another object available: the BrowserType object. This object lets the WebClass query the capabilities of the client browser, such as its support for ActiveX controls, cookies, and VBScript. All these capabilities are exposed as properties, as here:

If BrowserType.VBScript Then
    ' Send VBScript code to the client browser.
    ...
ElseIf BrowserType.JavaScript Then
    ' Send JavaScript code to the client browser.
    ...
End If

Other supported properties, whose names are self-explanatory, are Frames, Tables, Cookies, BackgroundSounds, JavaApplets, ActiveXControls, Browser (can return "IE" or "Netscape"), Version, MajorVersion, MinorVersion, and Platform (can return "Win95" or "WinNT"). This object relies on the Browscap.ini file that is installed with IIS in its main directory. Load it into an editor to get an idea of which properties are supported and the possible values they can have. Also remember to periodically visit Microsoft's Web site to download the most recent version of this file, which includes information on the newer browsers. You can also find an up-to-date version of this file at other Web sites, such as http://www.cyscape.com/browscap.

WebClass events

As with all classes, WebClass modules have their own life cycle. These are the relevant events in the life of a WebClass:

WebClasses offer no Load and Unload events; you can use the BeginRequest and EndRequest events for performing the tasks that you would typically perform in the Load and Unload events, respectively.

Tag replacement

One of the advantages of WebClasses over plain ASP programming is that you don't need to bury script code inside the HTML body of a page to create dynamic contents. At least for the simplest cases, WebClasses offer a better way.

If you need to send back to the client browser an HTML page that contains one or more variable parts—such as the name of the user, the total amount of an order, the details about a product—you just need to insert a pair of special tags in the HTML template page. When the WebClass processes the template, typically as the result of a WriteTemplate method, the corresponding WebItem object receives a series of ProcessTag events, one for each pair of the special tags. Inside this event, you can assign a value to a parameter, and that value will replace the text between the tags.

NOTE
The WriteTemplate method supports an optional Template argument, which lets you specify a different template to be returned to the client. This argument is useful when you have to choose among several templates with a number of events in common.

By default, the special tags that fire the ProcessTag events are <WC@tagname> and </WC@tagname>. WC@ is the tag prefix and is the same for all the tags of a given WebItem; tagname can vary from tag to tag and is used inside the ProcessTag event procedure to identify which particular pair of tags is to be replaced. Here's a fragment of an HTML template page that contains two pairs of such tags, which will be replaced by the user name and the current date and time:

<HTML><BODY>
Welcome back, <WC@USERNAME>Username</WC@USERNAME>. <P>
Current date/time is <WC@DATETIME></WC@DATETIME>
</BODY></HTML>

The text embedded between the opening and closing WC@ tag is the tag contents. Here's the code in the WebClass module that processes these tags and replaces them with meaningful information:

' This code assumes that the previous template is associated
' with a WebItem named WelcomeBack.
Private Sub WelcomeBack_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    Select Case TagName
        Case "WC@USERNAME"
            ' Replace with the user's name, held in a Session variable.
            TagContents = Session("UserName")
        Case "WC@DATETIME"
            ' Replace with the current date and time.
            TagContents = Format$(Now)
    End Select
End Sub

On entry to the event, the TagContents parameter holds the text found between the opening and closing WC@ tags. In most cases, you use this parameter only to output the replacement value, but nothing prevents you from using it to discern which kind of replacement should be done. For example, the QueryResults WebItem in the sample application on the companion CD uses a single tag—WC@FIELD—and uses the value of the TagContents parameter to retrieve the name of the database field that will be used to fill the various cells of a table. As you can see in the following code snippet, this approach simplifies the structure of the ProcessTag event procedure because you don't need to test the TagName parameter:

' The module-level rs variable points to the record that holds the results.
Private Sub QueryResults_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    If rs.EOF Then
        ' Don't display anything if there isn't a current record.
        TagContents = ""
    ElseIf TagContents = "Freight" Then
        ' This field needs special formatting.
        TagContents = FormatCurrency(rs(TagContents))
    Else
        ' All other fields can be output as they are, but we need to
        ' account for Null fields.
        TagContents = rs(TagContents) & ""
    End If
End Sub

An example of a result HTML page sent to the client browser is shown in Figure 20-17. Here are a few other details that concern tag substitution inside the ProcessTag event:

Custom events

Not all the requests the browser sends to the server can be directly mapped to a WebItem. In most cases, in fact, you'll probably want to process the request with some custom code and then decide which WebItem should be processed. In some cases, you don't even want to move to another WebItem, for example, when you process the data in a form and find that the information the user entered is incomplete or incorrect. In these situations, you need a custom event.

To create a custom event, you must right-click in the right pane of the WebClass designer on an attribute that is a candidate as an event source and select the Connect To Custom Event menu command. This operation creates the custom event and links the attribute to it in a single operation. You can then rename the custom event in the left pane of the designer, and the right pane will automatically update to reflect the new name. After you've created the custom event, you can double-click on it to enter some code for processing it. (You can also issue the View Code command from the pop-up menu.)

In the sample application provided on the companion CD, any time I need to process the Submit button in a form, I create a Submit custom event that processes the data the user entered and redisplays the same page—but with a suitable error message—if the data is incomplete or incorrect. For example, the following code in the Submit event of the QueryOrder WebItem checks whether the OrderID the user entered is an empty string and then retrieves the record in the Orders table that contains the information about that particular order. Notice that if the OrderID is empty or doesn't correspond to an existing order, the routine stores an error message in the QueryOrderMsg variable and then reprocesses the QueryOrder WebItem. This WebItem contains one replacement tag, which serves to display the error message (if an error message was stored in the QueryOrderMsg variable):

' The message that appears on top of the QueryOrder page
Dim QueryOrderMsg As String

' This event fires when the user enters an OrderID in the Query
' page and clicks the Submit button.
Private Sub QueryOrder_Submit()
    ' Don't accept a query with an empty order ID.
    If Request.Form("txtOrderID") = "" Then
        QueryOrderMsg = "Please insert an Order ID"
        QueryOrder.WriteTemplate
        Exit Sub
    End If
    
    OpenConnection
    ' This Recordset has to retrieve data from three different tables.
    rs.Open "SELECT OrderID, Customers.CompanyName As CompanyName," _
        & " OrderDate, RequiredDate, ShippedDate, Freight, " _
        & " Shippers.CompanyName As ShipVia, " _
        & "FROM Orders, Customers, Shippers " _
        & "WHERE Orders.CustomerID = Customers.CustomerID " _
        & "AND Orders.ShipVia = Shippers.ShipperID " _
        & "AND Orders.OrderID = " & Request.Form("txtOrderID")
    If rs.EOF Then
        ' No record matches the search criteria.
        CloseConnection
        QueryOrderMsg = "OrderID not found"
        QueryOrder.WriteTemplate
        Exit Sub
    End If
    ' If everything is OK, display the results.
    Set NextItem = QueryResults
End Sub

Private Sub QueryResults_Respond()
    ' Show the results, and then close the connection.
    QueryResults.WriteTemplate
    CloseConnection
End Sub

(See the previous section for the source code of the QueryResults_ProcessTag event procedure.) Two separate routines perform the actual opening and closing of the database connection:

' Open the connection to the database.
Private Sub OpenConnection()
    ' Close the Recordset if necessary.
    If rs.State And adStateOpen Then rs.Close
    ' If the connection is closed, open it.
    If (cn.State And adStateOpen) = 0 Then
        cn.Open "DSN=NorthWind"
        Set rs.ActiveConnection = cn
    End If
End Sub

' Close the Recordset and the connection.
Private Sub CloseConnection()
    If rs.State And adStateOpen Then rs.Close
    If cn.State And adStateOpen Then cn.Close
End Sub

Here's an important point to keep in mind: In general, you shouldn't store information in WebClass variables to share values among distinct event procedures, because if the StateManagement property is set to wcNoState, the WebClass is destroyed between consecutive calls and so are the values in the variables. The code above seems to violate this rule because it stores information in the QueryOrderMsg, cn, and rs variables. If you look more closely, however, you'll see that this information is never maintained across consecutive requests from clients, so this method of passing data is safe. For example, the QueryOrder_Submit event assigns a string to the QueryOrderMsg variable and then invokes the QueryOrder.WriteTemplate method. This method immediately fires the QueryOrder_ProcessTag event procedure, in which that variable is used. The same reasoning applies to the ADO Recordset that is opened in the QueryOrder_Submit event and closed in the QueryResults_Respond event.

Custom WebItems

As I mentioned earlier, there are two different types of WebItems: template WebItems and custom WebItems. Whereas template WebItems are always associated with an HTML template file, custom WebItems are made of Visual Basic code and generate an HTML page by using plain Response.Write methods. Needless to say, working with custom WebItems is more difficult than using template WebItems. Nevertheless, using custom WebItems pays off in terms of greater flexibility. For example, a custom WebItem is usually necessary when you want to create a table of results and you don't know in advance how many rows the table has.

A custom WebItem can be the target of an event from a template WebItem, and it exposes the Respond event and custom events as template WebItems do. For example, the Products custom WebItem in the sample application is the target of a hyperlink in the StartPage WebItem. The purpose of the Products WebItem is to provide a form in which the user can select a product category from a combo box control and enter the first characters of the desired product's name. (See Figure 20-18.) At first, you might think that you can display such a form using a template WebItem, but a closer look reveals that you need a custom WebItem because you have to fill the products combo box with the list of the product categories, something you can't do with the simple replacement approach permitted by template WebItems. The Products_Respond event procedure uses an auxiliary routine, named BuildProductsForm, that actually creates the form; the reason for using a separate procedure will become clear later:

' This event fires when the Products WebItem is reached
' from the Start page.
Private Sub Products_Respond()
    ' Display the Products form.
    BuildProductsForm False
End Sub

' Dynamically build the Products form; if the argument is True,
' the three controls are filled with data coming from Session variables.
Private Sub BuildProductsForm(UseSessionVars As Boolean)
    Dim CategoryID As Long, ProductName As String, SupplierName As String
    Dim selected As String
    If UseSessionVars Then
        CategoryID = Session("cboCategory")
        ProductName = Session("txtProduct")
        SupplierName = Session("txtSupplier")
    Else
        CategoryID = -1
    End If
    
    ' Build the page dynamically.
    Send "<HTML><BODY>"
    Send "<H1>Search the products we have in stock</H1>"
    Send "<FORM action=""@@1"" method=POST id=frmSearch name=frmSearch>", _
        URLFor("Products", "ListResults")
    Send "Select a category and/or type the first characters of the " _
        & "product's name or the supplier's name<P>"
    Send ""
    ' We need a table for alignment purposes.
    Send "<TABLE border=0 cellPadding=1 cellSpacing=1 width=75%>"
    Send "<TR>"
    Send "  <TD><DIV align=right>Select a category&nbsp; </DIV></TD>"
    Send "  <TD><SELECT name=cboCategory style=""HEIGHT: 22px; " _
        & "WIDTH: 180px"">"

    ' Fill the combo box with category names.
    ' The first item is selected only if CategoryID is -1.
    selected = IIf(CategoryID = -1, "SELECTED ", "")
    Send "<OPTION " & selected & "VALUE=-1>(All categories)"
    ' Then add all the records in the Categories table.
    OpenConnection
    rs.Open "SELECT CategoryID, CategoryName FROM Categories"
    ' Add all the categories to the combo box.
    Do Until rs.EOF
        selected = IIf(CategoryID = rs("CategoryID"), "SELECTED ", "")
        Send "    <OPTION @@1 value=@@2>@@3</OPTION>", selected, _
            rs("CategoryID"), rs("CategoryName")
        rs.MoveNext
    Loop
    rs.Close
    Send "</SELECT>"
    Send "</TD></TR>"
    
    ' Add the txtProduct text box, and fill it with the correct value.
    Send "<TR>"
    Send " <TD><DIV align=right>Product name&nbsp; </DIV></TD>"
    Send " <TD><INPUT name=txtProduct value=""@@1"" style=""HEIGHT: " _
        & " 22px; WIDTH: 176px""></TD></TR>", ProductName
    Send "<TR>"
    Send " <TD><DIV align=right>Supplier&nbsp;</DIV>"
    Send " <TD><INPUT name=txtSupplier value=""@@1"" style=""HEIGHT: " _
        & "22px; WIDTH: 177px"">", SupplierName
    Send "<TR><TD><TD>"
    Send "<TR>"
    Send " <TD><DIV align=right>&nbsp;</DIV></TD>"
    Send " <TD><INPUT type=submit value=""Search"" id=submit1 " _
        & "style=""HEIGHT: 25px; WIDTH: 90px"">"
    If BrowserType.VBScript Then
        Send "     <INPUT type=button value=""Reset fields"" id=btnReset" _
            & " Name=btnReset style=""HEIGHT: 25px; WIDTH: 90px"">"
    End If
    Send "</TD></TR></TABLE></P><P></P>"
    Send "</TABLE>"
    Send "</FORM>"
    Send "<HR>"

    ' Insert client-side script for the Reset Fields button.
    If BrowserType.VBScript Then
        Send "<SCRIPT LANGUAGE=VBScript>"
        Send "Sub btnReset_onclick()"
        Send "   frmSearch.cboCategory.Value = -1"
        Send "   frmSearch.txtProduct.Value = """""
        Send "   frmSearch.txtSupplier.Value = """""
        Send "End Sub"
    End If
    Send "</SCRIPT>"

    ' If this is a blank form, we must complete it.
    If Not UseSessionVars Then
        Send "<P><A href=""@@1"">go Back to the Welcome page</A>", _
            URLFor(Default)
        Send "</BODY></HTML>"
    End If
End Sub

Click to view at full size.

Figure 20-18. The form produced by the Products custom WebItem. The combo box control contains all the categories in the Categories table in the NorthWind database.

You'll probably agree that this isn't what you'd call "readable code." Still, it didn't take me much effort to create it. In fact, I just ran Microsoft InterDev (you can use your HTML editor of choice, of course) and created a sample form with three controls inside a table. (I used a table only for alignment purposes.) Then I imported the code in the Visual Basic code editor and wrote some code "around" the static HTML text. The entire process took about 10 minutes.

The previous routine has many interesting characteristics. First of all, to streamline the Visual Basic code, I created an auxiliary routine, named Send, which sends data to the output stream by using the Response.Write method. But the Send routine does a lot more; it also provides a way to dynamically replace variable portions in the output string, based on numbered placeholders. The routine is even capable of dealing with replaced arguments that occur inside a quoted string. These arguments must be processed in a special way because any double quote character inside them must be doubled in order to be displayed correctly on the client's browser. The complete source code of the routine follows. As you can see, the code isn't specific to this particular program and can therefore be easily reused in any other WebClass application.

' Send a string to the output stream, and substitute @@n placeholders
' with the arguments passed to the routine. (@@1 is replaced by the
' first argument, @@2 by the second argument, and so on.) Only one
' substitution per argument is permitted. If the @@n placeholder
' is enclosed with double quotes, any double quote is replaced by
' two consecutive double quotes.
Private Sub Send(ByVal Text As String, ParamArray Args() As Variant)
    Dim i As Integer, pos As Integer, placeholder As String
    For i = LBound(Args) To UBound(Args)
        placeholder = "@@" & Trim$(Str$(i + 1))
        ' First search the quoted placeholder.
        pos = InStr(Text, """" & placeholder)
        If pos Then
            ' Double all the quotes in the argument.
            pos = pos + 1
            Args(i) = Replace(Args(i), """", """""")
        Else
            ' Else, search the unquoted placeholder.
            pos = InStr(Text, placeholder)
        End If
        If pos Then
            ' If a placeholder found, substitute it with an argument.
            Text = Left$(Text, pos - 1) & Args(i) & Mid$(Text, pos + 3)
        End If
    Next
    ' Send the result text to the output stream.
    Response.Write Text & vbCrLf
End Sub

Another intriguing technique used in the BuildProductsForm routine is sending a piece of VBScript code to be processed on the client workstation when the user clicks the Reset Fields button. You can't rely on a standard button with TYPE=Reset because such a button would restore the contents that the fields have when the form is received from the server, and in some cases the server doesn't send blank fields to the client. For this reason, the only way to enable users to clear the fields on the form is to provide a button and associate a client-side script with it. Mixing server-side and client-side code is a powerful technique, and it also provides maximum scalability because it frees the server from the tasks that the client machine can conveniently perform. The client browser might not be capable of executing VBScript code, however, and for this reason the WebClass sends the client-script code only if the BrowserType.VBScript property returns True. A better approach is to send JavaScript code, which both Microsoft and Netscape browsers should accept.

The URLFor method

The last point of interest in the BuildProductsForm routine is where it defines what happens when the user clicks the Search button. As you know, both template WebItems and custom WebItems can expose custom events, which appear in the left pane of the WebClass designer. The way you create and invoke such custom events, however, is different for the two types of WebItems. When working with template WebItems, you implicitly create a custom event when you select the Connect To Custom Event menu command in the right pane of the designer. A custom WebItem creates its HTML code dynamically at run time, and therefore the designer can't display anything in the right pane. For this reason, you can create custom events for custom WebItems only manually, that is, by right-clicking on the WebItem and selecting the Add Custom Event menu command. (You can manually add a custom event to a template WebItem as well, if necessary.)

The Products WebItem in the sample application exposes two custom events, ListResults and RestoreResults, in addition to its standard Respond event. (See Figure 20-19.) The ListResults event fires when the user clicks the Search button, whereas the RestoreResults event fires when the user goes back to the Products page from the OrderRecap page. (See Figure 20-16.) When the user clicks the Search button, the Products WebItem uses the values in the form to dynamically build a table with all the products that match the search criteria and appends this table to the form itself.

Click to view at full size.

Figure 20-19. The WebClass designer, after adding the custom Products WebItem and its ListResults and RestoreResults custom events.

So here's the problem: How can you have the WebClass module fire the ListResult event in the Products WebItem when the user clicks the Search button? The answer to this question is the following line of code in the BuildProductsForm procedure:

Send "<FORM action=""@@1"" method=POST id=frmSearch name=frmSearch>", _
    URLFor("Products", "ListResults")

The URLFor method expects two arguments, the name of a WebItem and the name of an event, and generates a URL that—when the request is sent to the server—will fire that particular event for that particular WebItem. You can omit the second argument for this method; if you do, the WebClass will activate the default Response event.

TIP
The first argument of the URLFor method is defined as type Variant and can accept either a reference to a WebItem object or its name. For performance reasons, you should always pass the WebItem's name, as in the following example:

' The following two lines yield the same results, but the second
' line is slightly more efficient.
Response.Write URLFor(Products, "ListResults")
Response.Write URLFor("Products", "ListResults")

You respond to custom events in a custom WebItem as you would in a template WebItem. For example, the following code is executed when the user clicks the Search button in the Products WebItem. As you can see, it reuses the BuildProductsForm routine and then runs another auxiliary routine, BuildProductsTable, which generates the HTML table containing the results of the search.

' This event fires when the Product WebItem is invoked
' from the Search button on the form itself.
Private Sub Products_ListResults()
    ' Move data from controls on the form to Session variables.
    ' This allows you to return to the page later and reload these
    ' values in the controls.
    Session("cboCategory") = Request.Form("cboCategory")
    Session("txtProduct") = Request.Form("txtProduct")
    Session("txtSupplier") = Request.Form("txtSupplier")
    ' Rebuild the Products form, and then generate the result table.
    BuildProductsForm True
    BuildProductsTable
End Sub

' This private procedure builds the table that contains the
' result of the search on the Products table.
Private Sub BuildProductsTable()
    Dim CategoryID As Long, ProductName As String, SupplierName As String
    Dim selected As String, sql As String
    Dim records() As Variant, i As Long
    ' Retrieve the values from the Session variables.
    CategoryID = Session("cboCategory")
    ProductName = Session("txtProduct")
    SupplierName = Session("txtSupplier")
    
    ' Dynamically build the query string.
    sql = "SELECT ProductID, ProductName, CompanyName, QuantityPerUnit, " _
        & "UnitPrice FROM Products, Suppliers " _
        & "WHERE Products.SupplierID = Suppliers.SupplierID "
    If CategoryID <> -1 Then
        sql = sql & " AND CategoryID = " & CategoryID
    End If
    If ProductName <> "" Then
        sql = sql & " AND ProductName LIKE '" & ProductName & "%'"
    End If
    If SupplierName <> "" Then
        sql = sql & " AND CompanyName LIKE '" & SupplierName & "%'"
    End If
    ' Open the Recordset.
    OpenConnection
    rs.Open sql
    
    If rs.EOF Then
        Send "<B>No records match the specified search criteria.</B>"
    Else
        ' Read all the records in one operation.
        records() = rs.GetRows()
        ' Now we know how many products meet the search criteria.
        Send "<B>Found @@1 products.<B><P>", UBound(records, 2) + 1
        Send "You can order a product by clicking on its name."
    
        ' Build the result table.
        Send "<TABLE BORDER WIDTH=90%>"
        Send " <TR>"
        Send "  <TH WIDTH=35% ALIGN=left>Product</TH>"
        Send "  <TH WIDTH=30% ALIGN=left>Supplier</TH>"
        Send "  <TH WIDTH=25% ALIGN=left>Unit</TH>"
        Send "  <TH WIDTH=20% ALIGN=right>Unit Price</TH>"
        Send " </TR>"
        ' Add one row of cells for each record.
        For i = 0 To UBound(records, 2)
            Send " <TR>"
            Send "  <TD><A href=""@@1"">@@2</a></td>", _
                URLFor("OrderProduct", CStr(records(0, i))), records(1, i)
            Send "  <TD>@@1</TD>", records(2, i)
            Send "  <TD>@@1</TD>", records(3, i)
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(records(4, i))
            Send " </TR>"
        Next
        Send "</TABLE>"
    End If
    CloseConnection
    
    ' Complete the HTML page.
    Send "<P><A href=""@@1"">go Back to the Welcome page</A>", _
        URLFor("StartPage")
    Send "</BODY></HTML>"
End Sub

An example of the result of this event procedure is shown in Figure 20-20.

Click to view at full size.

Figure 20-20. A successful search in the Products table.

The UserEvent event

Let's continue analyzing the code in the BuildProductsTable. Notice that each product name in the leftmost column of the result table is a hyperlink created using this statement:

Send "  <TD><A href=""@@1"">@@2</a></td>", _
    URLFor("OrderProduct", CStr(records(0, i))), records(1, i)

The second argument passed to the URLFor method is the ProductID of the product whose name is made visible to the user. Obviously, the OrderProduct WebItem can't expose one event for each possible ProductID value, and in fact it doesn't need to. When the WebClass raises a WebItem event whose name doesn't correspond to either a standard event (such as Respond) or a custom event defined at design time, the WebItem element receives a UserEvent event. This event receives an EventName parameter that contains the name of the event specified as the second argument of the URLFor method. In this particular example, when the user clicks on a product name in the result table, the WebClass fires the OrderProduct_UserEvent event and passes it the ID of the selected product:

' This event fires when the user clicks on a product's name
' in the Products page, asking to order a given product.
' The name of the event is the ID of the product itself.
Private Sub OrderProduct_UserEvent(ByVal EventName As String)
    Dim sql As String
    ' Build the query string, and open the Recordset.
    sql = "SELECT ProductID, ProductName, CompanyName, QuantityPerUnit," _
        "UnitPrice FROM Products INNER JOIN Suppliers " _
        & " ON Products.SupplierID = Suppliers.SupplierID " _
        & "WHERE ProductID = " & EventName
    OpenConnection
    rs.Open sql
    ' Use the URLData property to send the ProductID to the page
    ' being shown in the browser. This value is then sent
    ' to the OrderRecap WebItem if the user confirms the inclusion
    ' of this product in the shopping bag.
    URLData = CStr(rs("ProductID"))
    ' Write the template. (This fires an OrderProduct_ProcessTag event.)
    OrderProduct.WriteTemplate
    CloseConnection
End Sub

Because OrderProduct is a template WebItem, the UserEvent procedure can execute the WebItem's WriteTemplate method, which in turn fires a ProcessTag event. The code inside this event procedure performs a tag replacement and fills a one-row table with data about the selected product. (See Figure 20-21.)

' The WebClass fires this event when the OrderProduct template is
' being interpreted. The only WC@ tag in this template is WC@FIELD, and the
' TagContents corresponds to the database field that must be displayed.
Private Sub OrderProduct_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    If TagContents = "UnitPrice" Then
        TagContents = FormatCurrency(rs("UnitPrice"))
    Else
        TagContents = rs(TagContents)
    End If
End Sub

Click to view at full size.

Figure 20-21. The result of the processing of the OrderProduct WebItem.

The URLData property

When the user enters the number of units of a given product to buy, the control jumps to the OrderRecap custom WebItem, which displays the list of all the items that are included in the current order and evaluates the total value of the order so far. To correctly implement this WebItem, you must solve a minor problem: how to pass the ID of the product selected by the user in the OrderProduct page. If the StateManagement property is set to wcRetainInstance, you can simply store it in a WebClass variable; but if the WebClass component is destroyed after the page has been sent back to the browser, you have to take a different approach.

Among the many techniques that you can adopt to preserve data among client requests, one of the simplest ones is based on the URLData property. When you assign a string to this property, the string is sent to the browser. When the browser sends the next request, the string is sent back to the server, and the WebClass can read it by querying the URLData property. In other words, the string assigned to this property isn't stored anywhere and continues to be pinged from the server to the client and back. This statement sets the URLData property in the OrderProduct_UserEvent procedure:

URLData = CStr(rs("ProductID"))

The ProductID value is then retrieved in the OrderRecap_Respond event procedure, where the application adds the new product to the current contents of the user's shopping bag. Such a shopping bag is implemented as a two-dimensional array stored in a Session variable:

Private Sub OrderRecap_Respond()
    ' The shopping bag is a two-dimensional array in a Session variable.
    ' This array has three rows: row 0 holds ProductID, row 1 holds
    ' Quantity, and row 2 holds UnitPrice. Each new product appends
    ' a new column.
    Dim shopBag As Variant, index As Integer, sql As String
    ' Retrieve the current shopping bag.
    shopBag = Session("ShoppingBag")

    If URLData <> "" Then
        ' Add a new product to the shopping bag.
        If IsEmpty(shopBag) Then
            ' This is the first product in the bag.
            ReDim shopBag(2, 0) As Variant
            index = 0
        Else
            ' Else extend the bag to include this product.
            index = UBound(shopBag, 2) + 1
            ReDim Preserve shopBag(2, index) As Variant
        End If
        ' Store the product in the array.
        shopBag(0, index) = URLData
        shopBag(1, index) = Request.Form("txtQty")
    End If
    
    ' Dynamically build the response page.
    Send "<HTML><BODY>"
    Send "<CENTER>"
    If IsEmpty(shopBag) Then
        ' No items are in the bag.
        Send "<H1>Your shopping bag is empty</H1>"
    Else
        ' Open the Products table to retrieve the products in the order.
        sql = "SELECT ProductID, ProductName, CompanyName, " _
            & "QuantityPerUnit, UnitPrice " _
            & "FROM Products INNER JOIN Suppliers " _
            & "ON Products.SupplierID = Suppliers.SupplierID "
        For index = 0 To UBound(shopBag, 2)
            sql = sql & IIf(index = 0, " WHERE ", " OR ")
            sql = sql & "ProductID = " & shopBag(0, index)
        Next
        OpenConnection
        rs.Open sql
        
        ' Build the table with the products in the shopping bag.
        Send "<H1>Your shopping bag contains the following items: </H1>"
        Send "<TABLE BORDER WIDTH=100%>"
        Send " <TR>"
        Send "  <TH WIDTH=5% ALIGN=center>Qty</TH>"
        Send "  <TH WIDTH=30% ALIGN=left>Product</TH>"
        Send "  <TH WIDTH=25% ALIGN=left>Supplier</TH>"
        Send "  <TH WIDTH=20% ALIGN=left>Unit</TH>"
        Send "  <TH WIDTH=10% ALIGN=right>Unit Price</TH>"
        Send "  <TH WIDTH=10% ALIGN=right>Price</TH>"
        Send " </TR>"
        
        ' Loop on all the records in the Recordset.
        Dim total As Currency, qty As Long
        Do Until rs.EOF
            ' Retrieve the quantity from the shopping bag.
            index = GetBagIndex(shopBag, rs("ProductID"))
            ' Remember the UnitPrice for later so that you don't need to
            ' reopen the Recordset when the order is confirmed.
            shopBag(2, index) = rs("UnitPrice")
            
            ' Get the requested quantity.
            qty = shopBag(1, index)
            ' Update the running total. (No discounts in this demo!)
            total = total + qty * rs("UnitPrice")
            ' Add a row to the table.
            Send " <TR>"
            Send "  <TD ALIGN=center>@@1</TD>", qty
            Send "  <TD ALIGN=left>@@1</TD>", rs("ProductName")
            Send "  <TD ALIGN=left>@@1</TD>", rs("CompanyName")
            Send "  <TD ALIGN=left>@@1</TD>", rs("QuantityPerUnit")
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(rs("UnitPrice"))
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(qty * rs("UnitPrice"))
            Send " </TR>"
            rs.MoveNext
        Loop
        CloseConnection
        
        ' Store the shopping bag back in the Session variable.
        Session("ShoppingBag") = shopBag
        ' Add a row for the total.
        Send " <TR>"
        Send "  <TD></TD><TD></TD><TD></TD><TD></TD>"
        Send "  <TD ALIGN=right><B>TOTAL</B></TD>"
        Send "  <TD ALIGN=right>@@1</TD>", FormatCurrency(total)
        Send " </TR>"
        Send "</TABLE><P>"
        ' Add a few hyperlinks.
        Send "<A href=""@@1"">confirm the order</A><P>", _
            URLFor("CustomerData")
        Send "<A href=""@@1"">cancel the order</A><P>", _
            URLFor("OrderCancel")
    End If
    
    Send "<A href=""@@1"">go back to the Search page</A>", _
        URLFor("Products", "RestoreResults")
    Send "</CENTER>"
    Send "</BODY></HTML>"
End Sub

The preceding routine uses an auxiliary function that searches for a ProductID value in the shopping bag and returns the corresponding column index, or -1 if the ProductID isn't found:

Function GetBagIndex(shopBag As Variant, ProductID As Long) As Long
    Dim i As Integer
    GetBagIndex = -1
    For i = 0 To UBound(shopBag, 2)
        If shopBag(0, i) = ProductID Then
            GetBagIndex = i
            Exit Function
        End If
    Next
End Function

The result of the processing of the OrderRecap WebItem is shown in Figure 20-22.

Click to view at full size.

Figure 20-22. The result of the processing of the OrderRecap WebItem, showing the current contents of the shopping bag.

The Professional Touch

With what you've learned so far, you can build fairly complex and powerful WebClass components. To create highly efficient and scalable applications, however, you need to learn a few additional details.

Navigation

A WebClass application differs from a standard application in many respects. One area in which the differences are remarkable is in navigating from one WebItem to another. In a traditional application, the programmer can control what the user can do at any moment, and no form in the program can be reached if the developer doesn't provide the user with a means to display it. In an Internet application, on the other hand, the user can navigate to any page by simply typing its URL in the browser's address field. This fact has a number of implications in the way you define the structure of the program:

State management

State management plays an important role in the development of WebClass applications. As you know, the HTTP protocol is inherently stateless, which means that it doesn't "remember" information from previous requests. As for regular ASP applications, when working with WebClasses you have several ways to overcome this issue, and each solution has advantages and disadvantages.

If the StateManagement property of the WebClass is set to wcRetainInstance, you can safely store all the information in the WebClass's variables because the instance of the WebClass is kept alive between client requests and will be destroyed only when the code explicitly invokes the ReleaseInstance method. You pay for this convenience with reduced scalability of the IIS application. Also, because WebClass components use the apartment threading model and can run only in the thread in which they were created, when a subsequent client request arrives, it might have to wait until that particular thread becomes available.

TIP
By default, IIS initially allocates 2 threads to ASP and increases this number as necessary, up to 10 threads per processor. You can modify the default values by assigning different numbers to the values for NumInitialThreads and ProcessorMaxThreads of the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\ASP\Parameters Registry key. The highest valid number for the ProcessorMaxThreads setting is 200.

You can store values in Application and Session variables so that they persist among client requests even if the WebClass component is destroyed and then re-created. If you have to store a lot of information, you might create a server-side component that stores the data and then assign it to an Application or a Session variable. In general, if the server-side component uses the apartment threading model—as all components authored in Visual Basic do—you shouldn't store them in an Application variable.

If you have to store a lot of data, you can resort to a database on the server machine. This solution permits the sharing of data among multiple clients, but it requires that you set up a connection and open a Recordset each time you need to read or write a value. Opening and closing a connection isn't as inefficient as it might sound, though, because database connections are pooled.

You can use the URLData property to move data back and forth between the server and the client, as explained in the section "The URLData Property" section, earlier in this chapter. This technique is equivalent to using the Request.QueryString property, but its implementation is considerably simpler. One of its advantages is that the data is stored in the page itself; therefore, if the user clicks the Back button and then resubmits the form, the WebClass receives the same data originally sent to the page. Another advantage is that the URLData property works even with browsers that don't support cookies. This technique isn't without disadvantages, however. You can't store more than about 2 KB of data in the URLData property, and moving data back and forth in this way slightly slows down each request. You can't use this technique if the HTML page includes a form whose METHOD attribute is set to GET, but this isn't a real limitation because WebClasses only work with forms that use the POST method.

You can use cookies, as you would in a regular ASP application—that is, through the Request.Cookies and the Response.Cookies collections. As is the case for the URLData property, you can pass only a limited amount of data through cookies. Even worse, the user might have disabled cookies for security reasons, or the browser might not support them at all (a rare occurrence, though). Moreover, you have a performance hit when moving many cookies with a lot of information in them, so you might be best off using cookies only to store the ID of a record that you later load from a database.

Another way to store state information in the page is by using an HTML Hidden Control, which the WebClass initializes when it creates the page and reads back when the page is resubmitted to the server, through the Request.Form collection of variables. The problems with this approach are that you can use it only when the page includes a form and that the contents of such hidden fields are visible in the source code of the page. If this visibility is a problem, you should encrypt the data stored in these fields.

Testing and deployment

Testing a WebClass application isn't much different from testing any ASP component in the sense that you can take advantage of all the debug tools offered by the Visual Basic environment. A couple of additional WebClass features are very useful in the debug phase.

The Trace method sends a string to the OutputDebugString Windows API function. A few debugging tools, such as the DBMON utility, can intercept such strings. Using the Trace method with a debugger in this way is especially useful after you've compiled the WebClass because you can't rely on other ways for displaying messages. Remember that WebClass applications use the Unattended Execution option therefore, you can't use MsgBox statements to display a message on the screen. However, you can use the methods of the App object to write to a log file or to the Windows NT event log.

When the WebClass application raises a fatal error (and therefore can't continue), the code receives a FatalErrorResponse event. You can react to this event by sending your custom message to the client browser using the Response.Write method, after which you should set the SendDefault argument to False to suppress the WebClass standard error message:

Private Sub WebClass_FatalErrorResponse(SendDefault As Boolean)
    Response.Write "A fatal error has occurred.<P>"
    Response.Write "If the problem persists, please send an e-mail"
    Response.Write "message to the Web administrator."
    SendDefault = False
End Sub

Inside a FatalErrorResponse event you can query the WebClass's Error object, which returns detailed information through its Number, Source, and Description properties. This object always returns Nothing outside the FatalErrorResponse event. All the fatal errors that can occur correspond to one of the wcrErrxxxx enumerated constants exposed by the WebClass library, such as wcrErrCannotReadHtml or wcrErrSystemError.

Fatal errors are automatically registered in the Windows NT event log, but you can disable this feature by changing the LogErrors value of the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Visual Basic\6.0\WebClass Registry key from 1 to 0. Under Windows 95 and Windows 98, a log file is created in the Windows directory.

Finally, you should account for some behavioral differences between the interpreted and the compiled versions of the WebClass component:

To have a compiled WebClass component execute correctly under IIS, you must distribute the special WebClass run-time file, which is contained in the Mswcrun.dll file.

We've finally come to the end of this long journey through Internet Information Server, ASP applications, and WebClass components. This chapter is the last one about Internet programming technologies. Both DHTML applications and WebClass components require a different approach than traditional programming, but in return you get the capability to write great Internet and intranet applications while still using your favorite programming language. Internet programming would be also the last topic covered in this book, but there are so many interesting ways to expand the Visual Basic language using Windows API functions, that I couldn't resist the temptation to include an Appendix exclusively devoted to these advanced techniques.